<?PHP
/**
 * OpenStation
 *
 * @Application : OpenTPL.class.php
 * @Encode : 65001 Unicode ( UTF-8 NO BOM )
 * @Description : OpenStation内置模板引擎
 * @Author : HK <hkyyang@gmail.com>
 */

class OpenTPL {

    const OPEN_TPL_VERSION = '1.3.4';

    // 模板标签开始符
    public $left;
    // 模板标签结束符
    public $right;
    // 当前模板数据
    public $data = array();
    // 所有模板数据
    public $multiData = array();
    // 内部行号标记
    public $internalLineTag;
    // 指定内部异常处理类
    public $internalException = 'OTException';
    // 主要模板
    public $mainTemplate = NULL;
    // 主要模板内部载入模板
    public $multiTemplate = NULL;

    // 默认的模板扩展名
    protected $templateFileExt;
    // 默认的静态文件扩展名
    protected $staticFileExt;
    // 当前默认模板文件名
    protected $templateFile;
    // 当前默认的模板目录
    public $templatePath;
    // 扩展函数目录
    public $ExtensionDir;
    // 当前默认的模板缓存目录
    protected $compilePath;
    // 模板缓存过期时间
    protected $templateLifeTime = -1;
    // 已经解析过的模板数据缓存
    protected $completeCache = array();

    // OpenTPL调试开关
    protected $debugEnabled = FALSE;
    // OpenTPL错误报告级别
    protected $errorReportLevel = 0;


    // 模板解析对象实例
    private $opParseObject = NULL;
    // OpenTPL自定义异常处理方法
    private $exceptionHandler = NULL;
    // 内部默认编码
    private $charset = NULL;



    /**
     * 构造方法
     *
     * @access public
     * @param Array : $config, 模板配置
     * @return void
     */
    public function __construct( $config = array() ) {

        // 设置模板引擎配置
        $this -> setConfig( $config );

        // 设置内部的文件编码
        if( function_exists( 'mb_internal_encoding' ) ) {
            mb_internal_encoding( $this -> charset );
        }
        // 定义内部行标记
        $this -> internalLineTag = substr( strtoupper( md5( microtime( TRUE ) ) ), -20 );
        // 模板解析对象实例
        $this -> opParseObject = new OpenTPL_Method( $this );
        $this -> ExtensionDir = OS_PATH . 'Lib' . DS . 'Template' . DS . 'Extension' . DS;

    }

    /**
     * setConfig 方法，设置模板引擎配置
     *
     * @access public
     * @param Array : $config, 模板配置
     * @return void
     */
    private function setConfig( $config = array() ) {
        $cfg = empty( $config ) ? Cache :: get( 'OPENSTATION.TEMPLATE' ) : array_change_key_case( $config, CASE_UPPER );
        $this -> templateFileExt = isset( $cfg[ 'EXT' ] ) ? $cfg[ 'EXT' ] : Cache :: get( 'OPENSTATION.TEMPLATE.EXT' );
        $this -> staticFileExt = isset( $cfg[ 'STATIC_EXT' ] ) ? $cfg[ 'STATIC_EXT' ] : Cache :: get( 'OPENSTATION.TEMPLATE.STATIC_EXT' );
        $this -> templateFile = strtolower( Cache :: get( 'SERVER.METHOD' ) ) . $this -> templateFileExt;
        $this -> templatePath = Cache :: get( 'APP_PATH.APP_TEMPLATE' );
        if( empty( $this -> templatePath ) ) {
            $this -> templatePath = Cache :: get( 'APP_PATH.TEMPLATE' ) . strtolower( Cache :: get( 'SERVER.ACTION_SHORT' ) ) . DS;
        }
        $this -> compilePath = Cache :: get( 'APP_PATH.COMPILE' );
        $this -> left = isset( $cfg[ 'LEFT_DELIMITER' ] ) ? $cfg[ 'LEFT_DELIMITER' ] : Cache :: get( 'OPENSTATION.TEMPLATE.LEFT_DELIMITER' );
        $this -> right = isset( $cfg[ 'RIGHT_DELIMITER' ] ) ? $cfg[ 'RIGHT_DELIMITER' ] : Cache :: get( 'OPENSTATION.TEMPLATE.RIGHT_DELIMITER' );
        $this -> templateLifeTime = isset( $cfg[ 'LIFE_TIME' ] ) ? intval( $cfg[ 'LIFE_TIME' ] ) : Cache :: get( 'OPENSTATION.TEMPLATE.LIFE_TIME' );
        $this -> charset = isset( $cfg[ 'CHARSET' ] ) ? $cfg[ 'CHARSET' ] : Cache :: get( 'OPENSTATION.TEMPLATE.CHARSET' );
        $this -> debugEnabled = Cache :: get( '@.ERROR.DEBUG' );
        $this -> errorReportLevel = Cache :: get( '@.ERROR.LEVEL' );
    }

    /**
     * fetch 方法，解析模板文件，返回模板缓存文件路径
     *
     * @access public
     * @param String : $tpl, 模板文件
     * @return String
     */
    public function fetch( $tpl = NULL ) {
        $this -> autoSetExceptionHandler();
        // 获取模板文件的绝对路径
        $fileTpl = $this -> getTemplateFile( $tpl );
        // 标记主要模板
        $this -> mainTemplate = $fileTpl;
        // 清除子模板
        $this -> multiTemplate = NULL;

        // 错误，模板文件不存在
        if( empty( $fileTpl ) || ! is_file( $fileTpl ) ) {
            $this -> opParseObject -> throwException( Cache :: lang( 'TEMPLATE_NOT_EXIST' ) . ': "' . $tpl . '"', NULL, FALSE );
        }
        // 获取模板缓存的文件路径
        $compile = $this -> getCompileFile( $fileTpl );
        // 检查并编译模板缓存
        $this -> updateCache( $compile, $fileTpl );
        $this -> restoreExceptionHandler();
        return $compile;
    }

    /**
     * multiFetch 方法，解析模板内载入的模板
     *
     * @access public
     * @param String : $tpl, 模板文件
     * @return Boolean
     */
    public function multiFetch( $tpl ) {

        $fileTpl = $this -> getTemplateFile( $tpl );

        // 模板验证
        if( ! is_file( $fileTpl ) ) {
            $this -> opParseObject -> throwException( Cache :: lang( 'TEMPLATE_NOT_EXIST' ) . ': "' . $tpl . '"', $this -> opParseObject -> InternalLine, FALSE );
        }
        // 错误尝试在当前模板载入自己
        if( $this -> mainTemplate == $fileTpl ) {
            $this -> opParseObject -> throwException( Cache :: lang( 'NOT_LOAD_SELF' ) . ': "' . $tpl . '"', $this -> opParseObject -> InternalLine, FALSE );
        }

        // 标记子模板
        $this -> multiTemplate = $fileTpl;

        // 获取模板缓存的文件路径
        $tplc = $this -> getCompileFile( $fileTpl );

        // 检查并编译模板缓存
        $this -> updateCache( $tplc, $fileTpl );
        // 数据切换
        $this -> data = array();
        if( isset( $this -> multiData[ $this -> mainTemplate ] ) ) {
            $this -> data = $this -> multiData[ $this -> mainTemplate ];
        }
        // 清除子模板
        $this -> multiTemplate = NULL;

        return TRUE;
    }

    /**
     * updateCache 方法，检查并编译模板缓存
     *
     * @access private
     * @param String : $tplc, 缓存文件
     * @param String : $tpl, 模板文件
     * @return Boolean
     */
    private function updateCache( $tplc, $tpl ) {
        $cname = md5( $tpl );
        // 已经解析过数据
        if( isset( $this -> completeCache[ $cname ] ) ) {
            return TRUE;
        }
        if( $this -> checkCache( $tplc, $tpl ) ) {
            $data = $this -> getTemplateData( $tpl );
            $compile = $this -> opParseObject -> parse( $data );
            $this -> completeCache[ $cname ] = $compile;
            $path = dirname( $tplc );
            mkdirs( $path );
            return file_put_contents( $tplc, $compile );
        }
        return TRUE;
    }

    /**
     * getTemplateCompile 方法，获取已经编译完成的模板缓存( 不更新缓存 )
     *
     * @access private
     * @param String : $tpl, 模板文件
     * @return String
     */
    public function getTemplateCompile( $tpl ) {
        $this -> autoSetExceptionHandler();
        // 获取模板文件绝对路径
        $file = $this -> getTemplateFile( $tpl );
        // 错误，模板文件不存在
        if( ! is_file( $file ) ) {
            $this -> opParseObject -> throwException( Cache :: lang( 'TEMPLATE_NOT_EXIST' ) . ': "' . $tpl . '"', NULL, FALSE );
        }
        $cname = md5( str_replace( array( '/', '\\' ), DS, $file ) );
        $this -> restoreExceptionHandler();
        // 返回已经解析过模板缓存
        if( isset( $this -> completeCache[ $cname ] ) ) {
            return $this -> completeCache[ $cname ];
        }
        return file_get_contents( $file );
    }

    /**
     * checkCache 方法，检查编译的模板缓存是否需要更新
     *
     * @access private
     * @param String : $tplc, 缓存文件
     * @param String : $tpl, 模板文件
     * @return String
     */
    private function checkCache( $tplc, $tpl ) {
        // 缓存文件不存在，需要更新
        if( ! is_file( $tplc ) ) {
            return TRUE;
        // 模板文件修改时间大于缓存，需要更新
        } else if( filemtime( $tplc ) < filemtime( $tpl ) ) {
            return TRUE;
        // 超过模板过期时间，需要更新
        } else if( $this -> templateLifeTime > -1 && time() - filectime( $tplc ) > $this -> templateLifeTime ) {
            return TRUE;
        }
        return FALSE;
    }

    /**
     * getCompileFile 方法，获取模板缓存的绝对路径
     *
     * @access public
     * @param String : $tpl, 模板文件
     * @return String
     */
    public function getCompileFile( $tpl ) {
        $tpl = $this -> getTemplateFile( $tpl );
        $name = get_file_name( $tpl );
        $compileFile = md5( str_replace( array( '/', '\\', '/\\', '\\/' ), '/', $tpl ) ) . '.' . $name . '.cache.php';
        return Cache :: get( 'APP_PATH.COMPILE' ) . $compileFile;
    }

    /**
     * getTemplateFile 方法，获取完整的模板文件路径
     *
     * @access public
     * @param String : $tpl, 模板文件
     * @return String
     */
    public function getTemplateFile( $tpl = NULL ) {
        $tpl = empty( $tpl ) ? $this -> templatePath . $this -> templateFile : $tpl;
        if( ! is_file( $tpl ) ) {
            $tpl = string_append_replace( $tpl, $this -> templateFileExt );
            if( is_file( $this -> templatePath . $tpl ) ) {
                $tpl = $this -> templatePath . $tpl;
            } else if( is_file( Cache :: get( 'APP_PATH.TEMPLATE' ) . $tpl ) ) {
                $tpl = Cache :: get( 'APP_PATH.TEMPLATE' ) . $tpl;
                $this -> templatePath = dirname( $tpl ) . DS;
            }
        }
        return str_replace( array( '/', '\\', '/\\', '\\/' ), DS, $tpl );
    }

    /**
     * callExtensionFunc方法，对模板变量使用扩展函数
     *
     * @access private
     * @param String : $path, 扩展目录
     * @param String : $name, 函数名称
     * @param String : $var, 原模板变量
     * @param Array : $params, 参数数组
     * @return String
     */
    public function callExtensionFunc( $path, $name, $var ) {
        static $_importExtension = array();
        $funcName = 'OpenTPL_function_' . $name;
        $file = $this -> ExtensionDir . string_append_replace( $path, DS ) . 'function.' . $name . '.php';
        $cname = md5( $path . $name );
        if( ! is_file( $file ) ) {
            return NULL;
        } else if( ! isset( $_importExtension[ $cname ] ) ) {
            import( $file, '.php' );
            $_importExtension[ $cname ] = $funcName;
        }
        $pNum = func_num_args();
        $temp = func_get_args();
        $params = array();
        for( $i = 2; $i < $pNum; $i ++ ) {
            $params[] = $temp[ $i ];
        }
        return call_user_func_array( $funcName, $params );
    }

    /**
     * getTemplateData 方法，获取模板内容并以行数组缓存
     *
     * @access private
     * @param String : $tpl, 模板文件
     * @return String
     */
    private function getTemplateData( $tpl ) {
        $this -> multiData[ $tpl ] = file( $tpl );
        $this -> data = $this -> multiData[ $tpl ];
        $data = '';
        if( ! empty( $this -> data ) ) {
            foreach( $this -> data as $line => $content ) {
                $rn = substr( $content, -1 );
                $content = rtrim( $content, "\r\n" );
                $data .= $content . $this -> left . $this -> internalLineTag . ( $line + 1 ) . $this -> right;
                if( ( $line + 1 ) != count( $this -> data ) ) {
                    $data .= $rn;
                }
            }
        }
        return $data;
    }

    /**
     * tplEchoURI 方法，组装并输出URL
     *
     * @access private
     * @param * $args
     * @return void
     */
    public function tplEchoURI() {
        $args = func_get_args();
        $url = array_shift( $args );

        for( $i = count( $args ) - 1; $i > -1; $i -- ) {
            if( strlen( $args[ $i ] ) < 1 ) {
                array_pop( $args );
            } else {
                break;
            }
        }

        switch( Cache :: get( '@.MAIN.URL_MODE' ) ) {
            case URL_NORMAL :
                $url .= '?' . implode( '&', $args );
                break;
            case URL_PATHINFO :
                $url .= Cache :: get( 'OPENSTATION.URL.PATHINFO_DEPR' ) . implode( Cache :: get( 'OPENSTATION.URL.PATHINFO_DEPR' ), $args );
                break;
            case URL_REWRITE :
                if( string_after_remove( basename( $url ), '.php' ) == 'index' ) {
                    $url = string_append_replace( dirname( $url ), '/' ) . implode( Cache :: get( 'OPENSTATION.URL.PATHINFO_DEPR' ), $args );
                } else {
                    $url .= Cache :: get( 'OPENSTATION.URL.PATHINFO_DEPR' ) . implode( Cache :: get( 'OPENSTATION.URL.PATHINFO_DEPR' ), $args );
                }
                break;
            case URL_AUTO :
                $url .= '?' . Cache :: get( 'OPENSTATION.URL.PATHINFO_AUTONAME' ) . '=' . implode( Cache :: get( 'OPENSTATION.URL.PATHINFO_DEPR' ), $args );
                break;
        }

        echo $url;

    }

    /**
     * setExceptionHandler 方法，设置模板的异常处理
     *
     * @access private
     * @param String : $handler
     * @return void
     */
    private function setExceptionHandler( $handler ) {
        if( ! class_exists( $this -> internalException, FALSE ) ) {
            import( 'Lib.Exception.' . $this -> internalException );
        }
        $this -> exceptionHandler = $handler[ 1 ];
        set_exception_handler( $handler );
    }

    /**
     * catchException 方法，异常捕获
     *
     * @access public
     * @param Exception : $e
     * @return *
     */
    public function catchException( $e ) {
        if( IS_CLI || ! $this -> debugEnabled ) {
            echo Cache :: lang( 'TEMPLATE_ERROR' ) . ': ' . $e -> getMessage();
        } else {
            $e = $e -> getReport();
            require Cache :: get( 'OPENSTATION.ERROR.TPL_ERROR' );
        }
        exit;
    }

    /**
     * autoSetExceptionHandler 方法，自动设置内部的异常错误处理
     *
     * @access private
     * @param $
     * @return void
     */
    private function autoSetExceptionHandler() {
        if( empty( $this -> exceptionHandler ) ) {
            $this -> setExceptionHandler( array( $this, 'catchException' ) );
        }
    }

    /**
     * restoreExceptionHandler 方法，还原上次的异常处理设置
     *
     * @access private
     * @param $
     * @return void
     */
    private function restoreExceptionHandler() {
        if( ! empty( $this -> exceptionHandler ) ) {
           restore_exception_handler();
        }
        // 清空记录
        $this -> exceptionHandler = NULL;
    }

    /**
     * 析构方法
     *
     * @access public
     * @return void
     */
    public function __destruct() {
        $this -> restoreExceptionHandler();
    }


}
?>